package com.icehelper.hades.base.util.httpfluent;

import com.icehelper.hades.base.log.HadesLogger;
import com.icehelper.hades.base.log.LogConstant;
import com.icehelper.hades.base.log.LogUtil;
import com.icehelper.hades.base.log.SpanIdGenerator;
import com.icehelper.hades.base.util.Inspections;
import com.icehelper.hades.base.util.JsonHelper;
import com.icehelper.hades.base.util.MapBuilder;
import com.icehelper.hades.base.util.httpfluent.config.HttpClientAutoConfig;
import com.icehelper.hades.base.util.httpfluent.constant.Constant;
import com.icehelper.hades.base.util.httpfluent.constant.InternalContext;
import lombok.Data;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.MDC;
import org.springframework.http.HttpMethod;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * httpFluent客户端
 */
@Data
public class HttpFluentUtil{

  private static final HadesLogger log = HadesLogger.create();

  private HttpFluentUtil() {
  }

  public static HttpFluentClient get(String url) {
    HttpFluentClient httpFluentClient = new HttpFluentClient();
    httpFluentClient.httpRequestBase = new HttpGet(url);
    return httpFluentClient;
  }

  public static HttpFluentClient post(String url) {
    HttpFluentClient httpFluentClient = new HttpFluentClient();
    httpFluentClient.httpRequestBase = new HttpPost(url);
    return httpFluentClient;
  }

  public static HttpFluentClient put(String url) {
    HttpFluentClient httpFluentClient = new HttpFluentClient();
    httpFluentClient.httpRequestBase = new HttpPut(url);
    return httpFluentClient;
  }

  public static HttpFluentClient delete(String url) {
    HttpFluentClient httpFluentClient = new HttpFluentClient();
    httpFluentClient.httpRequestBase = new HttpDelete(url);
    return httpFluentClient;
  }

  public static class HttpFluentClient {

    private final CloseableHttpClient httpClient;

    private final HttpContext httpContext = new BasicHttpContext();

    private List<NameValuePair> parameters;

    private final RequestConfig.Builder requestConfigBuilder;

    private int readTimeout;
    private int connectTimeout;

    private final MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();

    private volatile boolean isMultipart = false;

    private HttpRequestBase httpRequestBase;

    private Charset charset = Constant.CHARSET_UTF8;

    private HttpFluentClient() {
      HttpClientAutoConfig httpClientAutoConfig = HttpClientAutoConfig.getInstance();
      httpClient = httpClientAutoConfig.closeableHttpClient();
      requestConfigBuilder = RequestConfig.copy(httpClientAutoConfig.getRequestConfig());
      readTimeout = httpClientAutoConfig.getRequestConfig().getSocketTimeout();
      connectTimeout = httpClientAutoConfig.getRequestConfig().getConnectTimeout();
    }

    public HttpFluentClient addParameter(String name, String value) {
      Inspections.notBlank(name, "请求参数为空");
      if (parameters == null) {
        parameters = new ArrayList<>();
      }
      parameters.add(new BasicNameValuePair(name, value));
      return this;
    }

    public HttpFluentClient addParameters(Map<String, String> parameters) {
      Inspections.notNull(parameters, "请求参数为空");
      parameters.forEach(this::addParameter);
      return this;
    }

    public HttpFluentClient readTimeout(int readTimeout) {
      if (readTimeout > 0) {
        this.readTimeout = readTimeout;
      }
      return this;
    }

    public HttpFluentClient retry(int retry) {
      if (retry > 0) {
        context( InternalContext.RETRY.name(), retry);
      }
      return this;
    }

    public HttpFluentClient connectTimeout(int connectTimeout) {
      if (connectTimeout > 0) {
        this.connectTimeout = connectTimeout;
      }
      return this;
    }

    public HttpFluentClient proxy(String host, int port) {
      Inspections.notBlank(host, "请求主机为空");
      requestConfigBuilder.setProxy(new HttpHost(host, port));
      return this;
    }

    public HttpFluentClient proxy(String host) {
      return proxy(host, 80);
    }

    public HttpFluentClient body(HttpEntity entity) {
      Inspections.notNull(entity, Constant.EMPTY_REQUEST);
      if (httpRequestBase instanceof HttpEntityEnclosingRequest) {
        ((HttpEntityEnclosingRequest) httpRequestBase).setEntity(entity);
      } else {
        throw new IllegalStateException(httpRequestBase.getMethod() + " 请求无法设置HttpEntity");
      }
      return this;
    }

    public HttpFluentClient bodyBytes(byte[] bytes, ContentType contentType) {
      Inspections.notNull(bytes, Constant.EMPTY_REQUEST);
      HttpEntity httpEntity = new ByteArrayEntity(bytes, contentType);
      body(httpEntity);
      return this;
    }

    public HttpFluentClient bodyBytes(byte[] bytes) {
      return bodyBytes(bytes, ContentType.DEFAULT_BINARY);
    }

    public HttpFluentClient bodyInputStream(InputStream inputStream) {
      return bodyInputStream(inputStream, null);
    }

    public HttpFluentClient bodyInputStream(InputStream inputStream, ContentType contentType) {
      Inspections.notNull(inputStream, Constant.EMPTY_REQUEST);
      HttpEntity httpEntity = new InputStreamEntity(inputStream, contentType);
      body(httpEntity);
      return this;
    }

    public HttpFluentClient bodyFile(File file) {
      return bodyFile(file, ContentType.APPLICATION_OCTET_STREAM);
    }

    public HttpFluentClient bodyFile(File file, ContentType contentType) {
      Inspections.notNull(file, "请求文件为空");
      HttpEntity httpEntity = new FileEntity(file, contentType);
      body(httpEntity);
      return this;
    }

    public HttpFluentClient bodyString(String body, ContentType contentType) {
      Inspections.notNull(body, Constant.EMPTY_REQUEST);
      if (contentType.getCharset() != null) {
        charset = contentType.getCharset();
      }
      context(InternalContext.CHARSET.name(), charset);
      byte[] bytes = body.getBytes(charset);
      bodyBytes(bytes, contentType);
      return this;
    }

    public HttpFluentClient bodyString(String body) {
      return bodyString(body, Constant.TEXT_PLAIN);
    }

    public HttpFluentClient bodyJson(String body) {
      bodyString(body, Constant.APPLICATION_JSON);
      return this;
    }

    public HttpFluentClient bodyJson(String body, Charset charset) {

      bodyString(body, Constant.APPLICATION_JSON.withCharset(charset));
      return this;
    }

    public HttpFluentClient bodyObject(Object object) {
      return bodyObject(object, Constant.CHARSET_UTF8);
    }

    public HttpFluentClient bodyObject(Object object, Charset charset) {
      Inspections.notNull(object, "请求对象为空");
      return bodyJson( JsonHelper.toJson(object), charset);
    }

    public HttpFluentClient bodyText(String body) {
      bodyString(body, Constant.TEXT_PLAIN);
      return this;
    }

    public HttpFluentClient bodyText(String body, Charset charset) {
      bodyString(body, Constant.TEXT_PLAIN.withCharset(charset));
      return this;
    }

    public HttpFluentClient bodyHtml(String body) {
      bodyString(body, Constant.TEXT_HTML);
      return this;
    }

    public HttpFluentClient bodyHtml(String body, Charset charset) {
      bodyString(body, Constant.TEXT_HTML.withCharset(charset));
      return this;
    }

    public HttpFluentClient bodyXml(String body) {
      bodyString(body, Constant.APPLICATION_XML);
      return this;
    }

    public HttpFluentClient bodyXml(String body, Charset charset) {
      bodyString(body, Constant.APPLICATION_XML.withCharset(charset));
      return this;
    }

    public HttpFluentClient bodyForm(Map<String, String> form) {
      return bodyForm(form, Constant.CHARSET_UTF8);
    }

    public HttpFluentClient bodyForm(Map<String, String> form, Charset charset) {
      Inspections.notNull(form, "请求表单为空");
      List<NameValuePair> paramList = new ArrayList<>();
      form.forEach((name, value) -> paramList.add(new BasicNameValuePair(name, value)));
      String s = URLEncodedUtils.format(paramList, charset);
      bodyString(s, Constant.DEFAULT_CONTENT_TYPE);
      return this;
    }

    /**
     * @param key 表单的key
     * @param value 表单的值
     * @param fileName 文件的名字
     * @return HttpFluentClient
     */
    public HttpFluentClient bodyMultiPart(String key, Object value, String fileName) {

      if (value == null || key == null) {
        return this;
      }
      if (StringUtils.isBlank(fileName)) {
        fileName = key;
      }
      if (value instanceof String) {
        multipartEntityBuilder.addTextBody(key, (String) value);
      }
      if (value instanceof File) {
        isMultipart = true;
        multipartEntityBuilder.addBinaryBody(key, (File) value, ContentType.DEFAULT_BINARY, fileName);
      }
      if (value instanceof byte[]) {
        isMultipart = true;
        multipartEntityBuilder.addBinaryBody(key, (byte[]) value, ContentType.DEFAULT_BINARY, fileName);
      }
      if (value instanceof InputStream) {
        isMultipart = true;
        multipartEntityBuilder.addBinaryBody(key, (InputStream) value, ContentType.DEFAULT_BINARY, fileName);
      }
      return this;
    }

    public HttpFluentClient addHeader(String key, String value) {
      Inspections.notBlank(key, "请求http头为空");
      httpRequestBase.addHeader(key, value);
      return this;
    }

    public HttpFluentClient addHeaders(Map<String, String> headers) {
      Inspections.notNull(headers, "请求http头为空");
      headers.forEach((key, value) -> httpRequestBase.addHeader(key, value));
      return this;
    }

    public HttpFluentClient context(String key, Object value) {
      Inspections.notNull(key, "上下文的key为空");
      httpContext.setAttribute(key, value);
      return this;
    }

    public HttpFluentClient context(Map<String, Object> context) {
      Inspections.notNull(context, "上下文为空");
      context.forEach(this::context);
      return this;
    }

    public void abort() {
      httpRequestBase.abort();
    }

    private void initUrl() {
      if (parameters == null || parameters.isEmpty()) {
        return;
      }
      try {
        URI uri = new URIBuilder(httpRequestBase.getURI()).addParameters(parameters).build();
        httpRequestBase.setURI(uri);
      } catch (URISyntaxException e) {
        log.error("请求url转换异常", e);
      }
    }

    private void init() {
      requestConfigBuilder.setConnectTimeout(connectTimeout);
      requestConfigBuilder.setSocketTimeout(readTimeout);
      httpRequestBase.setConfig(requestConfigBuilder.build());
      initUrl();
      if (isMultipart) {
        HttpEntity httpEntity = multipartEntityBuilder.build();
        body(httpEntity);
      }
    }

    @Data
    private class ResponseStatus {

      String response;
      int statusCode;
    }

    private ResponseStatus handleResponse(CloseableHttpResponse response, Charset charset) throws IOException {
      final StatusLine statusLine = response.getStatusLine();
      final HttpEntity entity = response.getEntity();
      ResponseStatus responseStatus = new ResponseStatus();
      try {
        responseStatus.statusCode = statusLine.getStatusCode();
        responseStatus.response = (entity == null ? null : EntityUtils.toString(entity, charset));
      } finally {
        EntityUtils.consume(entity);
      }
      return responseStatus;
    }

    public String execute() {
      String urlPath = new URIBuilder(httpRequestBase.getURI()).toString();
      init();
      long beginTime = System.currentTimeMillis();
      String childSpanId = SpanIdGenerator.generatorSpanId();
      setHeaderKV(httpRequestBase, childSpanId);
      Throwable failureException = null;
      boolean success = true;
      int statusCode = -1;
      String response = StringUtils.EMPTY;
      try (CloseableHttpResponse closeableHttpResponse = httpClient.execute(httpRequestBase, httpContext)) {
        ResponseStatus responseStatus = handleResponse(closeableHttpResponse, charset);
        statusCode = responseStatus.statusCode;
        success = (statusCode >= 200 && statusCode < 300);
        response = responseStatus.response;
      } catch (IOException e) {
        failureException = e;
        success = false;
      } finally {
        if (HttpClientAutoConfig.getInstance().getHttpClientConfig().isLoggable()) {
          String content = "";
          if (Inspections.isEmpty(parameters) && HttpMethod.POST.name().equals(httpRequestBase.getMethod())) {
            ByteArrayEntity byteArrayEntity = (ByteArrayEntity) ((HttpEntityEnclosingRequest) httpRequestBase).getEntity();
            try {
              content = IOUtils.toString(byteArrayEntity.getContent(), StandardCharsets.UTF_8);
            } catch (IOException e) {
              log.error("read httpEntity content exception", e);
            }
          }
          httpTraceLog(
              beginTime,
              childSpanId,
              urlPath,
              failureException,
              success,
              Inspections.isEmpty(parameters) ? content : parameters,
              statusCode,
              response,
              null);
        }
        metricRpc(System.currentTimeMillis() - beginTime, success, urlPath);
      }
      return response;
    }

    /**
     * 发射rpc metric
     */
    private void metricRpc(Long procTime, Boolean success, String url) {
      try {

      } catch (Exception e) {
        log.debug("rpc metric exception", e);
      }
    }

    private static void setHeaderKV(HttpUriRequest httpUriRequest, final String childSpanId) {
      if (httpUriRequest == null) {
        return;
      }
      String traceId = LogUtil.getTraceId();
      if (StringUtils.isBlank(traceId)) {
        traceId = LogUtil.genTraceId();
      }
      httpUriRequest.setHeader( LogConstant.KEY_TRACE_ID, traceId);
      httpUriRequest.setHeader(LogConstant.KEY_SPAN_ID, childSpanId);
    }

    public byte[] asBytes() {
      return execute().getBytes();
    }

    public <T> T asObject(Class<T> clazz) {
      String result = execute();
      return StringUtils.isBlank(result) ? null : JsonHelper.parseObject(result, clazz);
    }

    /**
     * traceLog
     **/

    public static void httpTraceLog(long beginTime, String childSpanId, String urlPath,
        Throwable exception, boolean success, Object params, int statusCode, Object result,
        String serviceName) {
      try {
        String paramStr = toJsonString(params);
        String resultStr = toJsonString(result);
        final long endTime = System.currentTimeMillis();

        MapBuilder<String, String> kvBuilder =
            MapBuilder.create("service", "Http-Req")
                .add("interface", urlPath)
                .add("url", urlPath)
                .add("uri", LogUtil.getUri())
                .add("success", Objects.toString(success))
                .add("proc_time", Objects.toString(endTime - beginTime))
                .add("timestamp", Objects.toString(endTime))
                .add("statusCode", Objects.toString(statusCode))
                .add("childSpanId", childSpanId)
                .add(LogConstant.KEY_CHILD_SPAN_ID, childSpanId)
                .add(LogConstant.KEY_TRACE_ID, MDC.get(LogConstant.KEY_TRACE_ID))
                .add("service_name", serviceName);

        final int maxLength = 4096;
        if (Inspections.isNotBlank(paramStr)) {
          kvBuilder.add("rpcMethodArgs", StringUtils.substring(paramStr, 0, maxLength));
        }
        if (Inspections.isNotBlank(resultStr)) {
          kvBuilder.add("rpcMethodResult", StringUtils.substring(resultStr, 0, maxLength));
        }

        if (success) {
          log.addKvs(kvBuilder.build()).dlInfo(LogConstant.DLTAG_COM_HTTP_SUCCESS, "http call succeeded");
        } else {
          String dlTag;
          dlTag = LogConstant.DLTAG_COM_HTTP_FAILURE;
          if (exception instanceof SocketTimeoutException) {
            dlTag = "_com_http_SocketTimeoutException";
          } else if (exception instanceof ConnectTimeoutException) {
            dlTag = "_com_http_ConnectTimeoutException";
          } else if (exception instanceof ConnectException) { // typically connect refused
            dlTag = "_com_http_ConnectException";
          }
          kvBuilder.add("exception", Objects.toString(exception));
          log.addKvs(kvBuilder.build()).dlError(dlTag, "http call failed", exception);
        }
      } catch (Throwable t) {
        log.error("Error while trace log for http. ", t);
      }
    }

    private static String toJsonString(Object obj) {
      if (obj == null) {
        return "null";
      } else if (obj instanceof String) {
        return obj.toString();
      } else {
        return JsonHelper.toJson(obj);
      }
    }

    private static String localIp() {
      try {
        InetAddress localAddress = InetAddress.getLocalHost();
        return localAddress.getHostName();
      } catch (Throwable e) {
        log.warn("Error while get local address", e);
      }
      return null;
    }
  }
}
